const { join, isAbsolute, basename } = require('node:path')
const { existsSync, readFileSync } = require('node:fs')
const { removeSync } = require('fs-extra')
const { merge } = require('webpack-merge')
const { green } = require('kolorist')
const { build: esBuild, context: esContextBuild } = require('esbuild')
const debounce = require('lodash/debounce.js')

const appPaths = require('./app-paths.js')
const { appPkg, quasarPkg, updateAppPackageJson } = require('./app-pkg.js')
const { log, warn, fatal } = require('./utils/logger.js')
const { extensionsRunner } = require('./app-extension/extensions-runner.js')
const { appFilesValidations } = require('./utils/app-files-validations.js')
const { cssVariables } = require('./utils/css-variables.js')
const { getPackage } = require('./utils/get-package.js')
const getPackageMajorVersion = require('./utils/get-package-major-version.js')
const storeProvider = require('./utils/store-provider.js')
const { createWebpackConfig } = require('./webpack/index.js')
const { readFileEnv } = require('./utils/env.js')
const { quasarEsbuildInjectReplacementsDefine, quasarEsbuildInjectReplacementsPlugin } = require('./plugin.esbuild.inject-replacements.js')

const transformAssetUrls = getPackage('quasar/dist/transforms/loader-asset-urls.json')
const urlRegex = /^http(s)?:\/\//

const tempFile = `${ appPaths.quasarConfigFilename }.temporary.compiled.cjs`

const quasarConfigBanner = `/* eslint-disable */
/**
 * THIS FILE IS GENERATED AUTOMATICALLY.
 * 1. DO NOT edit this file directly as it won't do anything.
 * 2. EDIT the original quasar.config file INSTEAD.
 * 3. DO NOT git commit this file. It should be ignored.
 *
 * This file is still here because there was an error in
 * the original quasar.config file and this allows you to
 * investigate the Node.js stack error.
 *
 * After you fix the original file, this file will be
 * deleted automatically.
 **/
`

function getEsbuildConfig () {
  return {
    platform: 'node',
    format: 'cjs',
    bundle: true,
    packages: 'external',
    banner: {
      js: quasarConfigBanner
    },
    define: quasarEsbuildInjectReplacementsDefine,
    entryPoints: [ appPaths.quasarConfigFilename ],
    outfile: tempFile,
    plugins: [ quasarEsbuildInjectReplacementsPlugin ]
  }
}

function encode (obj) {
  return JSON.stringify(obj, (_, value) => {
    return typeof value === 'function'
      ? `/fn(${ value.toString() })`
      : value
  })
}

function clone (obj) {
  return JSON.parse(JSON.stringify(obj))
}

function escapeHTMLTagContent (str) {
  return str ? str.replace(/[<>]/g, '') : ''
}
function escapeHTMLAttribute (str) {
  return str ? str.replace(/\"/g, '') : ''
}

function formatPublicPath (publicPath) {
  if (!publicPath) {
    return ''
  }

  if (!publicPath.endsWith('/')) {
    publicPath = `${ publicPath }/`
  }

  if (urlRegex.test(publicPath) === true) {
    return publicPath
  }

  if (!publicPath.startsWith('/')) {
    publicPath = `/${ publicPath }`
  }

  return publicPath
}

function formatRouterBase (publicPath) {
  if (!publicPath || !publicPath.startsWith('http')) {
    return publicPath
  }

  const match = publicPath.match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/)
  return formatPublicPath(match[ 5 ] || '')
}

function parseAssetProperty (prefix) {
  return asset => {
    if (typeof asset === 'string') {
      return {
        path: asset[ 0 ] === '~' ? asset.substring(1) : prefix + `/${ asset }`
      }
    }

    return {
      ...asset,
      path: typeof asset.path === 'string'
        ? (asset.path[ 0 ] === '~' ? asset.path.substring(1) : prefix + `/${ asset.path }`)
        : asset.path
    }
  }
}

function getUniqueArray (original) {
  return Array.from(new Set(original))
}

function uniquePathFilter (value, index, self) {
  return self.map(obj => obj.path).indexOf(value.path) === index
}

function uniqueRegexFilter (value, index, self) {
  return self.map(regex => regex.toString()).indexOf(value.toString()) === index
}

module.exports.QuasarConfigFile = class QuasarConfigFile {
  ctx
  opts
  quasarConf
  webpackConf

  #isWatching = false
  #configSnapshot
  #webpackConfChanged
  #versions = {}

  constructor (ctx, opts = {}) {
    this.ctx = ctx
    this.opts = opts

    log(`Using ${ basename(appPaths.quasarConfigFilename) } in "${ appPaths.quasarConfigInputFormat }" format`)
  }

  // mimicking q/app-vite which is already in ESM format
  // for the future of q/app-webpack itself
  // (example: async here is useless, but it's here for consistency)
  async init () {
    if (this.ctx.mode.pwa) {
      this.#versions.workboxWebpackPlugin = getPackageMajorVersion('workbox-webpack-plugin')
    }
    else if (this.ctx.mode.capacitor) {
      const { capVersion } = require('./capacitor/cap-cli.js')
      const getCapPluginVersion = capVersion <= 2
        ? () => true
        : name => {
          const version = getPackageMajorVersion(name, appPaths.capacitorDir)
          return version === void 0
            ? false
            : version || true
        }

      Object.assign(this.#versions, {
        capacitor: capVersion,
        capacitorPluginApp: getCapPluginVersion('@capacitor/app'),
        capacitorPluginSplashscreen: getCapPluginVersion('@capacitor/splash-screen')
      })
    }
  }

  read () {
    const esbuildConfig = getEsbuildConfig()
    return this.opts.watch !== void 0
      ? this.#buildAndWatch(esbuildConfig)
      : this.#build(esbuildConfig)
  }

  // start watching for changes
  watch () {
    this.#isWatching = true
  }

  async #build (esbuildConfig) {
    try {
      await esBuild(esbuildConfig)
    }
    catch (e) {
      removeSync(tempFile)
      console.log()
      console.error(e)
      fatal('Could not compile quasar.config file because it has errors.', 'FAIL')
    }

    let fnResult
    try {
      fnResult = require(tempFile)
    }
    catch (e) {
      console.log()
      console.error(e)
      fatal(
        'The quasar.config file has runtime errors. Please check the Node.js stack above against the'
        + ` temporarily created ${ basename(tempFile) } file and fix the original file.`,
        'FAIL'
      )
    }

    const quasarConfigFn = fnResult.default || fnResult
    return this.#computeConfig(quasarConfigFn, true)
  }

  async #buildAndWatch (esbuildConfig) {
    let firstBuildIsDone

    esbuildConfig.plugins.push({
      name: 'quasar:watcher',
      setup: build => {
        let isFirst = true
        let updateFn

        const scheduleUpdate = debounce(() => {
          this.opts.watch[ updateFn ]()
          updateFn = null
        }, 1000)

        build.onStart(() => {
          if (isFirst === false) {
            log('The quasar.config file (or its dependencies) changed. Reading it...')
            updateAppPackageJson()
          }
        })

        build.onEnd(async result => {
          if (isFirst === false && this.#isWatching === false) {
            // not ready yet; watch() has not been issued yet
            return
          }

          if (result.errors.length !== 0) {
            removeSync(tempFile)

            const msg = 'Could not compile quasar.config file because it has errors.'

            if (isFirst === true) {
              fatal(msg, 'FAIL')
            }

            warn(msg + ' Please fix them.\n')
            return
          }

          let quasarConfigFn

          try {
            const result = require(tempFile)
            quasarConfigFn = result.default || result
          }
          catch (e) {
            // free up memory immediately
            delete require.cache[ tempFile ]

            console.log()
            console.error(e)

            const msg = 'The quasar.config file has runtime errors. Please check the'
              + ` Node.js stack above against the temporarily created ${ basename(tempFile) } file and fix the original file.`

            if (isFirst === true) {
              fatal(msg, 'FAIL')
            }

            warn(msg + '\n')
            return
          }

          // free up memory immediately
          delete require.cache[ tempFile ]

          const { quasarConf, webpackConf } = await this.#computeConfig(quasarConfigFn, isFirst)

          if (quasarConf === void 0) {
            return
          }

          if (isFirst === true) {
            isFirst = false
            firstBuildIsDone({ quasarConf, webpackConf })
            return
          }

          // if triggering updates too fast, then remember
          // if any of them required the rebuild
          updateFn = updateFn === 'onBuildChange' || this.#webpackConfChanged === true
            ? 'onBuildChange'
            : 'onAppChange'

          log('Scheduled to apply quasar.config changes in 1s')
          scheduleUpdate()
        })
      }
    })

    const esbuildCtx = await esContextBuild(esbuildConfig)
    await esbuildCtx.watch()

    return new Promise(res => { // eslint-disable-line promise/param-names
      firstBuildIsDone = res
    })
  }

  // return void 0 if it encounters errors
  // and { quasarConf, webpackConf } otherwise
  async #computeConfig (quasarConfigFn, failOnError) {
    if (typeof quasarConfigFn !== 'function') {
      removeSync(tempFile)

      const msg = 'The default export value of the quasar.config file is not a function.'

      if (failOnError === true) {
        fatal(msg, 'FAIL')
      }

      warn(msg + ' Please fix it.\n')
      return {}
    }

    let userCfg

    try {
      userCfg = await quasarConfigFn(this.ctx)
    }
    catch (e) {
      console.log()
      console.error(e)

      const msg = 'The quasar.config file has runtime errors.'
        + ' Please check the Node.js stack above against the'
        + ` temporarily created ${ basename(tempFile) } file.`

      if (failOnError === true) {
        fatal(msg, 'FAIL')
      }

      warn(msg + ' Please fix the errors in the original file.\n')
      return {}
    }

    if (Object(userCfg) !== userCfg) {
      removeSync(tempFile)

      const msg = 'The quasar.config file does not default exports an Object.'

      if (failOnError === true) {
        fatal(msg, 'FAIL')
      }

      warn(msg + ' Please fix it.\n')
      return {}
    }

    removeSync(tempFile)

    const rawQuasarConf = merge({
      ctx: this.ctx,

      css: [],
      boot: [],

      vendor: {
        add: [],
        remove: []
      },

      build: {
        transpileDependencies: [],
        vueLoaderOptions: {
          transformAssetUrls: {}
        },
        sassLoaderOptions: {},
        scssLoaderOptions: {},
        stylusLoaderOptions: {},
        lessLoaderOptions: {},
        tsLoaderOptions: {},
        tsCheckerOptions: {},
        env: {},
        rawDefine: {},
        envFiles: [],
        uglifyOptions: {
          compress: {},
          mangle: {}
        }
      },

      devServer: {
        server: {}
      },

      framework: {
        components: [],
        directives: [],
        plugins: []
      },

      animations: [],
      extras: [],
      sourceFiles: {},

      ssr: {
        middlewares: []
      },
      pwa: {},
      electron: {
        inspectPort: 5858,
        unPackagedInstallParams: [],
        packager: {},
        builder: {}
      },
      cordova: {},
      capacitor: {
        capacitorCliPreparationParams: []
      },
      bex: {
        builder: {
          directories: {}
        }
      },

      bin: {},
      htmlVariables: {}
    }, userCfg)

    const metaConf = {
      debugging: this.ctx.dev === true || this.ctx.debug === true,
      needsAppMountHook: false,
      vueDevtools: false,
      versions: { ...this.#versions }, // used by .quasar entry templates
      css: { ...cssVariables }
    }

    if (rawQuasarConf.animations === 'all') {
      const { animations } = require('./utils/animations.js')
      rawQuasarConf.animations = animations
    }

    try {
      await extensionsRunner.runHook('extendQuasarConf', async hook => {
        log(`Extension(${ hook.api.extId }): Extending quasar.config file...`)
        await hook.fn(rawQuasarConf, hook.api)
      })
    }
    catch (e) {
      console.log()
      console.error(e)

      if (failOnError === true) {
        fatal('One of your installed App Extensions failed to run', 'FAIL')
      }

      warn('One of your installed App Extensions failed to run.\n')
      return {}
    }

    const cfg = {
      ...rawQuasarConf,
      metaConf
    }

    if (this.ctx.dev) {
      if (this.opts.host) {
        cfg.devServer.host = this.opts.host
      }
      else if (!cfg.devServer.host) {
        cfg.devServer.host = '0.0.0.0'
      }

      if (this.opts.port) {
        cfg.devServer.port = this.opts.port
      }
      else if (!cfg.devServer.port) {
        cfg.devServer.port = 8080
      }

      if (
        this.address
        && this.address.from.host === cfg.devServer.host
        && this.address.from.port === cfg.devServer.port
      ) {
        cfg.devServer.host = this.address.to.host
        cfg.devServer.port = this.address.to.port
      }
      else {
        const addr = {
          host: cfg.devServer.host,
          port: cfg.devServer.port
        }
        const to = this.opts.onAddress !== void 0
          ? await this.opts.onAddress(addr)
          : addr

        // if network error while running
        if (to === null) {
          throw new Error('NETWORK_ERROR')
        }

        cfg.devServer = merge({}, cfg.devServer, to)
        this.address = {
          from: addr,
          to: {
            host: cfg.devServer.host,
            port: cfg.devServer.port
          }
        }
      }
    }

    // If watching for changes then determine the type of them (webpack or not).
    // The snapshot below should only contain webpack config:
    if (this.opts.watch !== void 0) {
      const newConfigSnapshot = [
        cfg.build ? encode(cfg.build) : '',
        cfg.ssr && cfg.ssr.pwa ? encode(cfg.ssr.pwa) : '',
        cfg.framework ? cfg.framework.autoImportComponentCase : '',
        cfg.devServer ? encode(cfg.devServer) : '',
        cfg.pwa ? encode(cfg.pwa) : '',
        cfg.electron ? encode(cfg.electron) : '',
        cfg.bex ? encode(cfg.bex) : '',
        cfg.htmlVariables ? encode(cfg.htmlVariables) : ''
      ].join('')

      if (this.#configSnapshot) {
        this.#webpackConfChanged = newConfigSnapshot !== this.#configSnapshot
      }

      this.#configSnapshot = newConfigSnapshot
    }

    const rawDefine = {
      // vue
      __VUE_OPTIONS_API__: cfg.build.vueOptionsApi !== false,
      __VUE_PROD_DEVTOOLS__: this.ctx.dev === true || this.ctx.debug === true,

      // quasar
      __QUASAR_VERSION__: JSON.stringify(quasarPkg.version),
      __QUASAR_SSR__: this.ctx.mode.ssr === true,
      __QUASAR_SSR_SERVER__: false,
      __QUASAR_SSR_CLIENT__: false,
      __QUASAR_SSR_PWA__: false,

      // vue-i18n
      __VUE_I18N_FULL_INSTALL__: true,
      __VUE_I18N_LEGACY_API__: true,
      __VUE_I18N_PROD_DEVTOOLS__: this.ctx.dev === true || this.ctx.debug === true,
      __INTLIFY_PROD_DEVTOOLS__: this.ctx.dev === true || this.ctx.debug === true
    }

    if (cfg.vendor.disable !== true) {
      cfg.vendor.add = cfg.vendor.add.length > 0
        ? new RegExp(cfg.vendor.add.filter(v => v).join('|'))
        : void 0

      cfg.vendor.remove = cfg.vendor.remove.length > 0
        ? new RegExp(cfg.vendor.remove.filter(v => v).join('|'))
        : void 0
    }

    if (cfg.css.length > 0) {
      cfg.css = cfg.css.filter(_ => _)
        .map(parseAssetProperty('src/css'))
        .filter(asset => asset.path)
        .filter(uniquePathFilter)
    }

    if (cfg.boot.length > 0) {
      cfg.boot = cfg.boot.filter(_ => _)
        .map(parseAssetProperty('boot'))
        .filter(asset => asset.path)
        .filter(uniquePathFilter)
    }

    if (cfg.extras.length > 0) {
      cfg.extras = getUniqueArray(cfg.extras)
    }

    if (cfg.animations.length > 0) {
      cfg.animations = getUniqueArray(cfg.animations)
    }

    if (![ 'kebab', 'pascal', 'combined' ].includes(cfg.framework.autoImportComponentCase)) {
      cfg.framework.autoImportComponentCase = 'kebab'
    }

    // special case where a component can be designated for a framework > config prop
    if (cfg.framework.config && cfg.framework.config.loading) {
      const component = cfg.framework.config.loading.spinner
      // Is a component and is a QComponent
      if (component !== void 0 && /^(Q[A-Z]|q-)/.test(component) === true) {
        cfg.framework.components.push(component)
      }
    }

    cfg.framework.components = getUniqueArray(cfg.framework.components)
    cfg.framework.directives = getUniqueArray(cfg.framework.directives)
    cfg.framework.plugins = getUniqueArray(cfg.framework.plugins)

    cfg.build = merge({
      vueLoaderOptions: {
        transformAssetUrls: clone(transformAssetUrls)
      },

      showProgress: true,
      productName: appPkg.productName,
      productDescription: appPkg.description,
      // need to force extraction for SSR due to
      // missing functionality in vue-loader
      extractCSS: this.ctx.prod || this.ctx.mode.ssr,
      sourceMap: this.ctx.dev,
      minify: this.ctx.prod && this.ctx.mode.bex !== true,
      distDir: join('dist', this.ctx.modeName),
      htmlFilename: 'index.html',
      // will mess up SSR
      vueRouterMode: 'hash',
      transpile: true,
      // transpileDependencies: [], // leaving here for completeness
      devtool: this.ctx.dev
        ? 'eval-cheap-module-source-map'
        : 'source-map',
      // env: {}, // leaving here for completeness
      uglifyOptions: {
        compress: {
          // turn off flags with small gains to speed up minification
          arrows: false,
          collapse_vars: false, // 0.3kb
          comparisons: false,
          computed_props: false,
          hoist_funs: false,
          hoist_props: false,
          hoist_vars: false,
          inline: false,
          loops: false,
          negate_iife: false,
          properties: false,
          reduce_funcs: false,
          reduce_vars: false,
          switches: false,
          toplevel: false,
          typeofs: false,

          // a few flags with noticeable gains/speed ratio
          // numbers based on out of the box vendor bundle
          booleans: true, // 0.7kb
          if_return: true, // 0.4kb
          sequences: true, // 0.7kb
          unused: true, // 2.3kb

          // required features to drop conditional branches
          conditionals: true,
          dead_code: true,
          evaluate: true
        },
        mangle: {
          safari10: true
        }
      }
    }, cfg.build)

    if (cfg.build.transpile === true) {
      cfg.build.transpileDependencies = cfg.build.transpileDependencies.filter(uniqueRegexFilter)
      cfg.metaConf.transpileBanner = green('yes (Babel)')
    }
    else {
      cfg.metaConf.transpileBanner = 'no'
    }

    cfg.metaConf.loadingBar = cfg.framework.plugins.includes('LoadingBar')

    if (this.ctx.dev || this.ctx.debug) {
      Object.assign(cfg.build, {
        minify: false,
        gzip: false
      })
    }
    // need to force extraction for SSR due to
    // missing functionality in vue-loader
    if (this.ctx.dev && !this.ctx.mode.ssr) {
      cfg.build.extractCSS = false
    }
    if (this.ctx.debug) {
      cfg.build.sourceMap = true
    }

    if (this.ctx.mode.ssr) {
      Object.assign(cfg.build, {
        vueRouterMode: 'history',
        gzip: false
      })
    }
    else if (this.ctx.mode.cordova || this.ctx.mode.capacitor || this.ctx.mode.electron || this.ctx.mode.bex) {
      Object.assign(cfg.build, {
        htmlFilename: 'index.html',
        vueRouterMode: 'hash',
        gzip: false
      })
    }

    if (!isAbsolute(cfg.build.distDir)) {
      cfg.build.distDir = appPaths.resolve.app(cfg.build.distDir)
    }

    if (this.ctx.mode.cordova || this.ctx.mode.capacitor) {
      cfg.build.packagedDistDir = join(cfg.build.distDir, this.ctx.targetName)
    }

    if (this.ctx.mode.cordova || this.ctx.mode.capacitor) {
      cfg.build.distDir = appPaths.resolve[ this.ctx.modeName ]('www')
    }
    else if (this.ctx.mode.electron || this.ctx.mode.bex) {
      cfg.build.packagedDistDir = cfg.build.distDir
      cfg.build.distDir = join(cfg.build.distDir, 'UnPackaged')
    }

    cfg.build.publicPath
      = cfg.build.publicPath && [ 'spa', 'pwa', 'ssr' ].includes(this.ctx.modeName)
        ? formatPublicPath(cfg.build.publicPath)
        : (cfg.build.vueRouterMode === 'hash' ? '' : '/')

    /* careful if you configure the following; make sure that you really know what you are doing */
    cfg.build.vueRouterBase = cfg.build.vueRouterBase !== void 0
      ? cfg.build.vueRouterBase
      : formatRouterBase(cfg.build.publicPath)

    /* careful if you configure the following; make sure that you really know what you are doing */
    cfg.build.appBase = cfg.build.appBase !== void 0
      ? cfg.build.appBase
      : cfg.build.publicPath

    cfg.sourceFiles = merge({
      rootComponent: 'src/App.vue',
      router: 'src/router/index',
      store: `src/${ storeProvider.pathKey }/index`,
      indexHtmlTemplate: 'src/index.template.html',
      pwaRegisterServiceWorker: 'src-pwa/register-service-worker',
      pwaServiceWorker: 'src-pwa/custom-service-worker',
      pwaManifestFile: 'src-pwa/manifest.json',
      electronMain: 'src-electron/electron-main',
      electronPreload: 'src-electron/electron-preload'
    }, cfg.sourceFiles)

    appFilesValidations(cfg)

    cfg.metaConf.storePackage = storeProvider.name

    // do we have a store?
    const storePath = appPaths.resolve.app(cfg.sourceFiles.store)
    cfg.store = (
      existsSync(storePath)
      || existsSync(storePath + '.js')
      || existsSync(storePath + '.ts')
    )

    // make sure we have preFetch in config
    cfg.preFetch = cfg.preFetch || false

    if (this.ctx.mode.capacitor & cfg.capacitor.capacitorCliPreparationParams.length === 0) {
      cfg.capacitor.capacitorCliPreparationParams = [ 'sync', this.ctx.targetName ]
    }

    if (this.ctx.mode.ssr) {
      cfg.ssr = merge({
        pwa: false,
        pwaOfflineHtmlFilename: 'offline.html', // do NOT use index.html as name!
        manualStoreHydration: false,
        manualPostHydrationTrigger: false,
        prodPort: 3000 // gets superseded in production by an eventual process.env.PORT
      }, cfg.ssr)

      if (cfg.ssr.manualPostHydrationTrigger !== true) {
        cfg.metaConf.needsAppMountHook = true
      }

      if (cfg.ssr.middlewares.length > 0) {
        cfg.ssr.middlewares = cfg.ssr.middlewares.filter(_ => _)
          .map(parseAssetProperty('src-ssr/middlewares'))
          .filter(asset => asset.path)
          .filter(uniquePathFilter)
      }

      if (cfg.ssr.pwa) {
        const { installMissing } = require('./mode/install-missing.js')
        await installMissing('pwa')
        rawDefine.__QUASAR_SSR_PWA__ = true
      }

      this.ctx.mode.pwa = cfg.ctx.mode.pwa = !!cfg.ssr.pwa

      if (this.ctx.dev && cfg.devServer.server.type === 'https') {
        const { options } = cfg.devServer.server

        if (options === void 0) {
          const { getCertificate } = await import('@quasar/ssl-certificate')
          const sslCertificate = getCertificate({ log, fatal })
          cfg.devServer.server.options = {
            key: sslCertificate,
            cert: sslCertificate
          }
        }
        else {
          // we now check if config is specifying a file path
          // and we actually read the contents so we can later supply correct
          // params to the node HTTPS server
          [ 'ca', 'pfx', 'key', 'cert' ].forEach(prop => {
            if (typeof options[ prop ] === 'string') {
              try {
                options[ prop ] = readFileSync(options[ prop ])
              }
              catch (e) {
                console.error(e)
                console.log()
                delete options[ prop ]
                warn(`The devServer.server.options.${ prop } file could not be read. Removed the config.`)
              }
            }
          })
        }
      }
    }

    if (this.ctx.dev) {
      const originalSetup = cfg.devServer.setupMiddlewares
      const openInEditor = require('launch-editor-middleware')

      if (this.ctx.mode.bex === true) {
        cfg.devServer.devMiddleware = cfg.devServer.devMiddleware || {}
        cfg.devServer.devMiddleware.writeToDisk = true
      }

      cfg.devServer = merge({
        hot: true,
        allowedHosts: 'all',
        compress: true,
        open: true,
        client: {
          overlay: {
            warnings: false
          }
        },
        server: {
          type: 'http'
        },
        devMiddleware: {
          publicPath: cfg.build.publicPath,
          stats: false
        }
      },
      this.ctx.mode.ssr === true
        ? {
            devMiddleware: {
              index: false
            },
            static: {
              serveIndex: false
            }
          }
        : {
            historyApiFallback: cfg.build.vueRouterMode === 'history'
              ? { index: `${ cfg.build.publicPath || '/' }${ cfg.build.htmlFilename }` }
              : false,
            devMiddleware: {
              index: cfg.build.htmlFilename
            }
          },
      cfg.devServer,
      {
        setupMiddlewares: (middlewares, opts) => {
          const { app } = opts

          if (!this.ctx.mode.ssr) {
            const express = require('express')

            if (cfg.build.ignorePublicFolder !== true) {
              app.use((cfg.build.publicPath || '/'), express.static(appPaths.resolve.app('public'), {
                maxAge: 0
              }))
            }

            if (this.ctx.mode.cordova) {
              const folder = appPaths.resolve.cordova(`platforms/${ this.ctx.targetName }/platform_www`)
              app.use('/', express.static(folder, { maxAge: 0 }))
            }
          }

          app.use('/__open-in-editor', openInEditor(void 0, appPaths.appDir))

          return originalSetup
            ? originalSetup(middlewares, opts)
            : middlewares
        }
      })

      // (backward compatibility for upstream)
      // webpack-dev-server 4.5.0 introduced a change in behavior
      // along with deprecation notices; so we transform it automatically
      // for a better experience for our developers
      if (cfg.devServer.https !== void 0) {
        const { https } = cfg.devServer

        delete cfg.devServer.https

        if (https !== false) {
          cfg.devServer.server = {
            type: 'https'
          }

          if (Object(https) === https) {
            cfg.devServer.server.options = https
          }
        }
      }

      if (this.ctx.vueDevtools === true || cfg.devServer.vueDevtools === true) {
        cfg.metaConf.needsAppMountHook = true
        cfg.metaConf.vueDevtools = {
          host: cfg.devServer.host === '0.0.0.0' ? 'localhost' : cfg.devServer.host,
          port: 8098
        }
      }

      // make sure the prop is not supplied to webpack dev server
      if (cfg.devServer.vueDevtools !== void 0) {
        delete cfg.devServer.vueDevtools
      }

      if (this.ctx.mode.cordova || this.ctx.mode.capacitor || this.ctx.mode.electron) {
        cfg.devServer.open = false

        if (this.ctx.mode.electron) {
          cfg.devServer.server.type = 'http'
        }
      }

      if (cfg.devServer.open) {
        const { isMinimalTerminal } = require('./utils/is-minimal-terminal.js')
        if (isMinimalTerminal) {
          cfg.devServer.open = false
        }
      }

      if (cfg.devServer.open) {
        cfg.metaConf.devServer = {
          open: !!cfg.devServer.open,
          openOptions: cfg.devServer.open !== true
            ? cfg.devServer.open
            : false
        }
        cfg.devServer.open = false
      }
      else {
        cfg.metaConf.devServer = {}
      }
    }

    if (cfg.build.gzip) {
      const gzip = cfg.build.gzip === true
        ? {}
        : cfg.build.gzip
      let ext = [ 'js', 'css' ]

      if (gzip.extensions) {
        ext = gzip.extensions
        delete gzip.extensions
      }

      cfg.build.gzip = merge({
        algorithm: 'gzip',
        test: new RegExp('\\.(' + ext.join('|') + ')$'),
        threshold: 10240,
        minRatio: 0.8
      }, gzip)
    }

    if (this.ctx.mode.pwa) {
      cfg.pwa = merge({
        workboxMode: 'GenerateSW',
        injectPwaMetaTags: true,
        swFilename: 'sw.js', // should be .js (as it's the distribution file, not the input file)
        manifestFilename: 'manifest.json',
        useCredentialsForManifestTag: false
      }, cfg.pwa)

      if (![ 'GenerateSW', 'InjectManifest' ].includes(cfg.pwa.workboxMode)) {
        const msg = `Workbox strategy "${ cfg.pwa.workboxMode }" is invalid. `
          + 'Valid quasar.config file > pwa > workboxMode options are: GenerateSW or InjectManifest.'

        if (failOnError === true) {
          fatal(msg, 'FAIL')
        }

        warn(msg + ' Please fix it.\n')
        return {}
      }

      cfg.pwa.manifest.icons = (cfg.pwa.manifest.icons || []).map(icon => {
        if (urlRegex.test(icon.src) === false || (cfg.build.publicPath && icon.src.startsWith(cfg.build.publicPath) === false)) {
          icon.src = `${ cfg.build.publicPath }${ icon.src }`.replaceAll('//', '/')
        }
        return icon
      })
    }

    if (this.ctx.dev) {
      const urlPath = cfg.build.vueRouterMode === 'hash'
        ? (cfg.build.htmlFilename !== 'index.html' ? (cfg.build.publicPath ? '' : '/') + cfg.build.htmlFilename : '')
        : ''

      cfg.metaConf.getUrl = hostname => `http${ cfg.devServer.server.type === 'https' ? 's' : '' }://${ hostname }:${ cfg.devServer.port }${ cfg.build.publicPath }${ urlPath }`
      cfg.build.APP_URL = cfg.metaConf.getUrl(
        cfg.devServer.host === '0.0.0.0'
          ? 'localhost'
          : cfg.devServer.host
      )
    }
    else if (this.ctx.mode.cordova || this.ctx.mode.capacitor || this.ctx.mode.bex) {
      cfg.build.APP_URL = 'index.html'
    }
    else if (this.ctx.mode.electron) {
      rawDefine[ 'process.env.APP_URL' ] = '"file://" + __dirname + "/index.html"'
    }

    cfg.build.rawDefine = {
      ...rawDefine,
      ...cfg.build.rawDefine
    }

    Object.assign(cfg.build.env, {
      NODE_ENV: this.ctx.prod ? 'production' : 'development',
      CLIENT: true,
      SERVER: false,
      DEV: this.ctx.dev,
      PROD: this.ctx.prod,
      DEBUGGING: this.ctx.debug || this.ctx.dev,
      MODE: this.ctx.modeName,
      VUE_ROUTER_MODE: cfg.build.vueRouterMode,
      VUE_ROUTER_BASE: cfg.build.vueRouterBase,
      APP_URL: cfg.build.APP_URL
    })

    if (this.ctx.mode.pwa) {
      cfg.build.env.SERVICE_WORKER_FILE = `${ cfg.build.publicPath }${ cfg.pwa.swFilename }`
      cfg.metaConf.pwaManifestFile = appPaths.resolve.app(cfg.sourceFiles.pwaManifestFile)
    }
    else if (this.ctx.mode.bex) {
      cfg.bex = merge({}, cfg.bex, {
        builder: {
          directories: {
            input: cfg.build.distDir,
            output: join(cfg.build.packagedDistDir, 'Packaged')
          }
        }
      })
    }
    else if (this.ctx.mode.electron && this.ctx.prod) {
      const bundler = require('./electron/bundler.js')

      const icon = appPaths.resolve.electron('icons/icon.png')
      const builderIcon = process.platform === 'linux'
        // backward compatible (linux-512x512.png)
        ? (existsSync(icon) === true ? icon : appPaths.resolve.electron('icons/linux-512x512.png'))
        : appPaths.resolve.electron('icons/icon')

      cfg.electron = merge({
        packager: {
          asar: true,
          icon: appPaths.resolve.electron('icons/icon'),
          overwrite: true
        },
        builder: {
          appId: 'quasar-app',
          icon: builderIcon,
          productName: appPkg.productName || appPkg.name || 'Quasar App',
          directories: {
            buildResources: appPaths.resolve.electron('')
          }
        }
      }, cfg.electron, {
        packager: {
          dir: cfg.build.distDir,
          out: cfg.build.packagedDistDir
        },
        builder: {
          directories: {
            app: cfg.build.distDir,
            output: join(cfg.build.packagedDistDir, 'Packaged')
          }
        }
      })

      if (cfg.ctx.bundlerName) {
        cfg.electron.bundler = cfg.ctx.bundlerName
      }
      else if (!cfg.electron.bundler) {
        cfg.electron.bundler = bundler.getDefaultName()
      }

      if (this.opts.argv !== void 0) {
        const { ensureElectronArgv } = require('./utils/ensure-argv.js')
        ensureElectronArgv(cfg.electron.bundler, this.opts.argv)
      }

      if (cfg.electron.bundler === 'packager') {
        if (cfg.ctx.targetName) {
          cfg.electron.packager.platform = cfg.ctx.targetName
        }
        if (cfg.ctx.archName) {
          cfg.electron.packager.arch = cfg.ctx.archName
        }
      }
      else {
        cfg.electron.builder = {
          config: cfg.electron.builder
        }

        if (cfg.ctx.targetName === 'mac' || cfg.ctx.targetName === 'darwin' || cfg.ctx.targetName === 'all') {
          cfg.electron.builder.mac = []
        }

        if (cfg.ctx.targetName === 'linux' || cfg.ctx.targetName === 'all') {
          cfg.electron.builder.linux = []
        }

        if (cfg.ctx.targetName === 'win' || cfg.ctx.targetName === 'win32' || cfg.ctx.targetName === 'all') {
          cfg.electron.builder.win = []
        }

        if (cfg.ctx.archName) {
          cfg.electron.builder[ cfg.ctx.archName ] = true
        }

        if (cfg.ctx.publish) {
          cfg.electron.builder.publish = cfg.ctx.publish
        }
      }

      bundler.ensureInstall(cfg.electron.bundler)
    }

    cfg.htmlVariables = merge({
      ctx: cfg.ctx,
      process: { env: cfg.build.env },
      productName: escapeHTMLTagContent(cfg.build.productName),
      productDescription: escapeHTMLAttribute(cfg.build.productDescription)
    }, cfg.htmlVariables)

    cfg.metaConf.html = {
      minifyOptions: cfg.build.minify
        ? {
            removeComments: true,
            collapseWhitespace: true,
            removeAttributeQuotes: true,
            collapseBooleanAttributes: true,
            removeScriptTypeAttributes: true
          // more options:
          // https://github.com/kangax/html-minifier#options-quick-reference
          }
        : false
    }

    if (this.ctx.mode.capacitor && cfg.metaConf.versions.capacitorPluginSplashscreen && cfg.capacitor.hideSplashscreen !== false) {
      cfg.metaConf.needsAppMountHook = true
    }

    const { fileEnv, usedEnvFiles, envFromCache } = readFileEnv({
      quasarMode: this.ctx.modeName,
      buildType: this.ctx.dev ? 'dev' : 'prod',
      envFolder: cfg.build.envFolder,
      envFiles: cfg.build.envFiles
    })

    cfg.metaConf.fileEnv = fileEnv

    if (envFromCache === false && usedEnvFiles.length !== 0) {
      log(`Using .env files: ${ usedEnvFiles.join(', ') }`)
    }

    this.quasarConf = cfg

    if (this.#webpackConfChanged !== false) {
      this.webpackConf = await createWebpackConfig(cfg)
    }

    return {
      quasarConf: this.quasarConf,
      webpackConf: this.webpackConf
    }
  }
}
